// Protocol Buffers - Google's data interchange format
// Copyright 2008 Google Inc.  All rights reserved.
// http://code.google.com/p/protobuf/
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are
// met:
//
//     * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
//     * Redistributions in binary form must reproduce the above
// copyright notice, this list of conditions and the following disclaimer
// in the documentation and/or other materials provided with the
// distribution.
//     * Neither the name of Google Inc. nor the names of its
// contributors may be used to endorse or promote products derived from
// this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

package com.feinno.superpojo.io;

import com.feinno.superpojo.util.HexUtil;

import java.io.*;
import java.nio.BufferOverflowException;
import java.nio.ByteBuffer;
import java.nio.ReadOnlyBufferException;
import java.util.List;

/**
 * Immutable array of bytes. <br>
 * 
 * @author crazybob@google.com Bob Lee
 * @author kenton@google.com Kenton Varda
 */
public final class ByteString {
	private final byte[] bytes;

	private ByteString(final byte[] bytes) {
		this.bytes = bytes;
	}

	/**
	 * Gets the byte at the given index.
	 * 
	 * @throws ArrayIndexOutOfBoundsException
	 *             {@code index} is < 0 or >= size
	 */
	public byte byteAt(final int index) {
		return bytes[index];
	}

	/**
	 * Gets the number of bytes.
	 */
	public int size() {
		return bytes.length;
	}

	/**
	 * Returns {@code true} if the size is {@code 0}, {@code false} otherwise.
	 */
	public boolean isEmpty() {
		return bytes.length == 0;
	}

	// =================================================================
	// byte[] -> ByteString

	/**
	 * Empty ByteString.
	 */
	public static final ByteString EMPTY = new ByteString(new byte[0]);

	/**
	 * Copies the given bytes into a {@code ByteString}.
	 */
	public static ByteString copyFrom(final byte[] bytes, final int offset, final int size) {
		final byte[] copy = new byte[size];
		System.arraycopy(bytes, offset, copy, 0, size);
		return new ByteString(copy);
	}

	/**
	 * Copies the given bytes into a {@code ByteString}.
	 */
	public static ByteString copyFrom(final byte[] bytes) {
		return copyFrom(bytes, 0, bytes.length);
	}

	/**
	 * Copies {@code size} bytes from a {@code java.nio.ByteBuffer} into a
	 * {@code ByteString}.
	 */
	public static ByteString copyFrom(final ByteBuffer bytes, final int size) {
		final byte[] copy = new byte[size];
		bytes.get(copy);
		return new ByteString(copy);
	}

	/**
	 * Copies the remaining bytes from a {@code java.nio.ByteBuffer} into a
	 * {@code ByteString}.
	 */
	public static ByteString copyFrom(final ByteBuffer bytes) {
		return copyFrom(bytes, bytes.remaining());
	}

	/**
	 * Encodes {@code text} into a sequence of bytes using the named charset and
	 * returns the result as a {@code ByteString}.
	 */
	public static ByteString copyFrom(final String text, final String charsetName) throws UnsupportedEncodingException {
		return new ByteString(text.getBytes(charsetName));
	}

	/**
	 * Encodes {@code text} into a sequence of UTF-8 bytes and returns the
	 * result as a {@code ByteString}.
	 */
	public static ByteString copyFromUtf8(final String text) {
		try {
			return new ByteString(text.getBytes("UTF-8"));
		} catch (UnsupportedEncodingException e) {
			throw new RuntimeException("UTF-8 not supported?", e);
		}
	}

	/**
	 * Concatenates all byte strings in the list and returns the result.
	 * 
	 * <p>
	 * The returned {@code ByteString} is not necessarily a unique object. If
	 * the list is empty, the returned object is the singleton empty
	 * {@code ByteString}. If the list has only one element, that
	 * {@code ByteString} will be returned without copying.
	 */
	public static ByteString copyFrom(List<ByteString> list) {
		if (list.size() == 0) {
			return EMPTY;
		} else if (list.size() == 1) {
			return list.get(0);
		}

		int size = 0;
		for (ByteString str : list) {
			size += str.size();
		}
		byte[] bytes = new byte[size];
		int pos = 0;
		for (ByteString str : list) {
			System.arraycopy(str.bytes, 0, bytes, pos, str.size());
			pos += str.size();
		}
		return new ByteString(bytes);
	}

	// =================================================================
	// ByteString -> byte[]

	/**
	 * Copies bytes into a buffer at the given offset.
	 * 
	 * @param target
	 *            buffer to copy into
	 * @param offset
	 *            in the target buffer
	 */
	public void copyTo(final byte[] target, final int offset) {
		System.arraycopy(bytes, 0, target, offset, bytes.length);
	}

	/**
	 * Copies bytes into a buffer.
	 * 
	 * @param target
	 *            buffer to copy into
	 * @param sourceOffset
	 *            offset within these bytes
	 * @param targetOffset
	 *            offset within the target buffer
	 * @param size
	 *            number of bytes to copy
	 */
	public void copyTo(final byte[] target, final int sourceOffset, final int targetOffset, final int size) {
		System.arraycopy(bytes, sourceOffset, target, targetOffset, size);
	}

	/**
	 * Copies bytes into a ByteBuffer.
	 * 
	 * @param target
	 *            ByteBuffer to copy into.
	 * @throws ReadOnlyBufferException
	 *             if the {@code target} is read-only
	 * @throws BufferOverflowException
	 *             if the {@code target}'s remaining() space is not large enough
	 *             to hold the data.
	 */
	public void copyTo(ByteBuffer target) {
		target.put(bytes, 0, bytes.length);
	}

	/**
	 * Copies bytes to a {@code byte[]}.
	 */
	public byte[] toByteArray() {
		final int size = bytes.length;
		final byte[] copy = new byte[size];
		System.arraycopy(bytes, 0, copy, 0, size);
		return copy;
	}

	/**
	 * Constructs a new read-only {@code java.nio.ByteBuffer} with the same
	 * backing byte array.
	 */
	public ByteBuffer asReadOnlyByteBuffer() {
		final ByteBuffer byteBuffer = ByteBuffer.wrap(bytes);
		return byteBuffer.asReadOnlyBuffer();
	}

	/**
	 * Constructs a new {@code String} by decoding the bytes using the specified
	 * charset.
	 */
	public String toString(final String charsetName) throws UnsupportedEncodingException {
		return new String(bytes, charsetName);
	}

	/**
	 * Constructs a new {@code String} by decoding the bytes using the specified
	 * charset.
	 */
	public String toString() {
		return HexUtil.toHexString(toByteArray());
	}

	/**
	 * Constructs a new {@code String} by decoding the bytes as UTF-8.
	 */
	public String toStringUtf8() {
		try {
			return new String(bytes, "UTF-8");
		} catch (UnsupportedEncodingException e) {
			throw new RuntimeException("UTF-8 not supported?", e);
		}
	}

	// =================================================================
	// equals() and hashCode()

	@Override
	public boolean equals(final Object o) {
		if (o == this) {
			return true;
		}

		if (!(o instanceof ByteString)) {
			return false;
		}

		final ByteString other = (ByteString) o;
		final int size = bytes.length;
		if (size != other.bytes.length) {
			return false;
		}

		final byte[] thisBytes = bytes;
		final byte[] otherBytes = other.bytes;
		for (int i = 0; i < size; i++) {
			if (thisBytes[i] != otherBytes[i]) {
				return false;
			}
		}

		return true;
	}

	private volatile int hash = 0;

	@Override
	public int hashCode() {
		int h = hash;

		if (h == 0) {
			final byte[] thisBytes = bytes;
			final int size = bytes.length;

			h = size;
			for (int i = 0; i < size; i++) {
				h = h * 31 + thisBytes[i];
			}
			if (h == 0) {
				h = 1;
			}

			hash = h;
		}

		return h;
	}

	// =================================================================
	// Input stream

	/**
	 * Creates an {@code InputStream} which can be used to read the bytes.
	 */
	public InputStream newInput() {
		return new ByteArrayInputStream(bytes);
	}

	/**
	 * Creates a {@link CodedInputStream} which can be used to read the bytes.
	 * Using this is more efficient than creating a {@link CodedInputStream}
	 * wrapping the result of {@link #newInput()}.
	 */
	public CodedInputStream newCodedInput() {
		// We trust CodedInputStream not to modify the bytes, or to give anyone
		// else access to them.
		return CodedInputStream.newInstance(bytes);
	}

	// =================================================================
	// Output stream

	/**
	 * Creates a new {@link Output} with the given initial capacity.
	 */
	public static Output newOutput(final int initialCapacity) {
		return new Output(new ByteArrayOutputStream(initialCapacity));
	}

	/**
	 * Creates a new {@link Output}.
	 */
	public static Output newOutput() {
		return newOutput(32);
	}

	/**
	 * Outputs to a {@code ByteString} instance. Call {@link #toByteString()} to
	 * create the {@code ByteString} instance.
	 */
	public static final class Output extends FilterOutputStream {
		private final ByteArrayOutputStream bout;

		/**
		 * Constructs a new output with the given initial capacity.
		 */
		private Output(final ByteArrayOutputStream bout) {
			super(bout);
			this.bout = bout;
		}

		/**
		 * Creates a {@code ByteString} instance from this {@code Output}.
		 */
		public ByteString toByteString() {
			final byte[] byteArray = bout.toByteArray();
			return new ByteString(byteArray);
		}
	}

	/**
	 * Constructs a new ByteString builder, which allows you to efficiently
	 * construct a {@code ByteString} by writing to a {@link CodedOutputStream}.
	 * Using this is much more efficient than calling {@code newOutput()} and
	 * wrapping that in a {@code CodedOutputStream}.
	 * 
	 * <p>
	 * This is package-private because it's a somewhat confusing interface.
	 * Users can call {@link Message#toByteString()} instead of calling this
	 * directly.
	 * 
	 * @param size
	 *            The target byte size of the {@code ByteString}. You must write
	 *            exactly this many bytes before building the result.
	 */
	static CodedBuilder newCodedBuilder(final int size) {
		return new CodedBuilder(size);
	}

	/** See {@link ByteString#newCodedBuilder(int)}. */
	static final class CodedBuilder {
		private final CodedOutputStream output;
		private final byte[] buffer;

		private CodedBuilder(final int size) {
			buffer = new byte[size];
			output = CodedOutputStream.newInstance(buffer);
		}

		public ByteString build() {
			output.checkNoSpaceLeft();

			// We can be confident that the CodedOutputStream will not modify
			// the
			// underlying bytes anymore because it already wrote all of them.
			// So,
			// no need to make a copy.
			return new ByteString(buffer);
		}

		public CodedOutputStream getCodedOutput() {
			return output;
		}
	}
}
